package com.lsf.config.shiro;

import com.lsf.common.api.CommonApi;
import com.lsf.common.constant.CommonConstant;
import com.lsf.modules.base.vo.LoginUser;
import com.lsf.util.JwtUtil;
import com.lsf.util.RedisUtil;
import com.lsf.util.SpringContextUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.Set;

/**
 * 用户登录鉴权和获取用户授权
 *
 * @author Alexander
 */
@Component
@Slf4j
public class ShiroRealm extends AuthorizingRealm {

    @Lazy
    @Resource
    private CommonApi commonApi;

    @Lazy
    @Resource
    private RedisUtil redisUtil;

    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JwtToken;
    }

    /**
     * 授权
     * 权限信息认证(包括角色以及权限) 用户访问controller的时候进行的验证(在redis中存储权限信息)
     * 触发校验用户权限时才会调用此方法
     *
     * @param principals 身份信息
     * @return AuthorizationInfo 权限信息
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        String username = null;
        if (principals != null) {
            LoginUser sysUser = (LoginUser) principals.getPrimaryPrincipal();
            username = sysUser.getUsername();
        }
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        //设置用户拥有的角色集合
        Set<String> roles = commonApi.queryUserRoles(username);
        log.info(" Roles => " + roles);
        info.setRoles(roles);
        //设置用户拥有的权限集合
        Set<String> permissions = commonApi.queryUserAuths(username);
        log.info(" Permissions => " + permissions);
        info.setStringPermissions(permissions);
        return info;
    }

    /**
     * 认证
     * 用户信息认证是在用户进行登录的时候进行的验证(不存redis)
     * 验证用户输入的账号、密码是否正确,错误抛出异常
     *
     * @param auth 身份验证令牌
     * @return AuthenticationInfo 封装用户信息的AuthenticationInfo实例
     * @throws AuthenticationException 身份验证异常
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
        String token = (String) auth.getCredentials();
        if (token == null) {
            log.info("身份认证失败");
            throw new AuthenticationException("token为空!");
        }
        //校验token有效性
        LoginUser sysUser = null;
        try {
            sysUser = this.checkUserTokenIsEffect(token);
        } catch (AuthenticationException e) {
            JwtUtil.responseError(SpringContextUtils.getHttpServletResponse(), 401, e.getMessage());
            log.warn(e.getMessage());
            return null;
        }
        return new SimpleAuthenticationInfo(sysUser, token, getName());
    }

    /**
     * 验证token的有效性
     *
     * @param token
     * @return LoginUser
     */
    private LoginUser checkUserTokenIsEffect(String token) throws AuthenticationException {
        String username = JwtUtil.getUsername(token);
        if (username == null) {
            throw new AuthenticationException("token非法无效!");
        }
        //查询用户信息
        LoginUser user = commonApi.getUserByName(username);
        if (user == null) {
            throw new AuthenticationException("用户不存在!");
        }
        //判断用户状态
        if (user.getStatus() != 1) {
            throw new AuthenticationException("账号已被冻结,请联系管理员!");
        }
        //验证token是否超时失效或者账号密码是否错误
        if (!jwtTokenRefresh(token, username, user.getPassword())) {
            throw new AuthenticationException(CommonConstant.TOKEN_IS_INVALID_MSG);
        }
        return user;
    }

    /**
     * JWTToken刷新生命周期(实现用户在线操作不掉线功能)
     * 1、登陆成功后将用户的JWT生成的token作为K、V存储到redis里(这时候K、V值一样),缓存有效期设置为JWT有效期的2倍
     * 2、当该用户再次请求时,通过JWTFilter层层校验之后会进入到doGetAuthenticationInfo进行身份验证
     * 3、当该用户这次请求JWT生成的token值已经超时,但该token对应redis中的K还存在,则表示该用户一直在操作只是JWT的token失效了,程序会给该token对应的K映射的Value值重新生成JWTToken并覆盖,并重置失效时间
     * 4、当该用户这次请求JWT生成的token值已经超时,并且redis中不存在对应的Key,则表示该用户的账户空闲超时,返回的用户信息已失效,则需重新登陆
     * 注意：前端Http请求的Header中设置Authorization保持不变,校验有效性以redis中的token为准
     * 用户过期时间 = JWT有效时间 * 2
     *
     * @param token    令牌
     * @param username 用户名
     * @param password 密码
     * @return {@link Boolean}
     */
    private Boolean jwtTokenRefresh(String token, String username, String password) {
        String cacheToken = String.valueOf(redisUtil.get(CommonConstant.PREFIX_USER_TOKEN + token));
        if (StringUtils.isNotEmpty(cacheToken)) {
            //校验token有效性
            if (!JwtUtil.verify(token, username, password)) {
                String newAuthorization = JwtUtil.sign(username, password);
                //重置token并设置超时时间
                redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, newAuthorization);
                redisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + token, JwtUtil.EXPIRE_TIME * 2 / 1000);
            }
            return true;
        }
        return false;
    }

    /**
     * 清除当前用户的权限认证缓存
     *
     * @param principals 身份信息
     */
    @Override
    protected void clearCache(PrincipalCollection principals) {
        super.clearCache(principals);
    }
}
